package taskmaster.job;

import taskmaster.auth.User;
import taskmaster.util.LookupLists;
import taskmaster.util.TaskEnumeration;
import taskmaster.util.exception.InvalidTaskException;
import taskmaster.util.exception.TaskStatusSyncException;

import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.*;

/**
 * taskmaster.job.TaskCollection
 * <BR><BR>
 * A collection of Tasks, as determined (and created) by TaskBuilder, which gets its data from sql.
 * <BR><BR>
 * <B>DS updated this 030305 to include 'iteration' of jobs.  Check javadocs for TaskBuilder to see details of how iteration was implemented, and its flaws.</B>
 *
 * @see TaskBuilder
 */
public class TaskCollection extends taskmaster.util.RunStatement
{

    /**
     * The central data structure of a TaskCollection; its tasks.
     * <BR>All calls to this Collection go to the Tasks stored in this hashtable.
     */
    private final Map<String, Task> tasks = new Hashtable<String, Task>();
    /**
     * The set of User defined criteria that TaskBuilder used to create this Collection.
     */
    private QueryParams limiters = null;
    /**
     * Utility used throughout taskmaster as an abstraction from the database.
     */
    private LookupLists ll = null;
    /**
     * a list of distinct job tags that the tasks in this collection belong to
     */
    private String[] distinctJobTagsArray = null;

    /**
     * no arg constructor so that this class can be instantiated by the jsp.
     * <BR><BR>
     *
     * @see #replaceTaskHash()
     * @see #replaceLookupLists()
     */
    public TaskCollection()
    { }

    /** replaces the central datastructure of this Collection with newTaskHash, and the QueryParams that created it, limiters.
     * @param newTaskHash a hashtable of Tasks that now belong to this Collection.
     * @param limiters the criteria that TaskBuilder used to create the hashtable of Tasks.
     */
    /**
     * replaces the central datastructure of this Collection with newTaskHash, and the QueryParams that created it, limiters.
     *
     * @param newTaskHash a hashtable of Tasks that now belong to this Collection.
     * @param limiters    the criteria that TaskBuilder used to create the hashtable of Tasks.
     */
    public void replaceTaskHash(Hashtable<String, Task> newTaskHash, QueryParams limiters)
    {
        this.tasks.clear();
        if (newTaskHash != null)
        {
            this.tasks.putAll(newTaskHash);
        }
        this.limiters = limiters;
        this.setSortOrder(this.ORDER_BY);
        this.determineDistinctJobTags();// added 021222 -- when collection is updated, list of jobs should be updated also
    }
    /** replaces the internal reference to the utility class LookupLists with this one.*/
    /**
     * replaces the internal reference to the utility class LookupLists with this one.
     */
    public void replaceLookupLists(LookupLists ll)
    {
        this.ll = ll;
    }

    /**
     * gets the QueryParams used to create this collection
     *
     * @return the QueryParams used to create this collection
     */
    public QueryParams getLimiters()
    { return this.limiters; }

    /**
     * looks for this Task in this collection.  Lookup done with task.getKey().
     * <BR><BR>Not used because collections are kept 1 to a session.
     *
     * @return true if this Collection contains this Task, false otherwise.
     * @see getTask()
     */
    public boolean containsTask(Task task)
    {
        if (task == null)
        {
            return false;
        }
        return this.tasks.containsKey(task.getJobTag());
    }

    /**
     * returns the Task associated with this taskTag, or null if its not in this Collection
     *
     * @see containsTask()
     */
    public Task getTaskUsingKey(String taskKey)
    {
        return this.tasks.get(taskKey);
    }

    /**
     * returns the Task associated with this taskTag/jobTag combo, or null if its not in this Collection
     */
    public Task getTask(String jobTag, String taskTag)
    {
        return this.tasks.get(jobTag + ":" + taskTag);
    }

    /**
     * returns the number of Tasks in the Hashtable
     *
     * @return the number of Tasks in the Hashtable
     */
    public int size()
    {
        return this.tasks.size();
    }

    /**
     * true if the internal hashtable of tasks is null or has no elements; false otherwise.
     */
    public boolean isEmpty()
    {
        return this.tasks.isEmpty();
    }

    /**
     * When the tasks in this Collection are sorted, they are sorted according to this field.
     * <BR><BR>Changing how tasks are sorted changes this field's value to one of the ORDER_BY_* fields.
     */
    public int ORDER_BY = ORDER_BY_TASK_TAG;
    /**
     * Tasks in this collection are sortable by this field
     */
    public static final int ORDER_BY_TASK_TAG = 0;
    /**
     * Tasks in this collection are sortable by this field
     */
    public static final int ORDER_BY_JOB_TAG = 1;
    /**
     * Tasks in this collection are sortable by this field
     */
    public static final int ORDER_BY_WRITABLE = 3;
    /**
     * NOT IMPLEMENTED!!! - Tasks in this collection could be sortable by this field if you want to add such an option.
     */
    public static final int ORDER_BY_TYPE = 4;//
    /**
     * NOT IMPLEMENTED!!! - Tasks in this collection could be sortable by this field if you want to add such an option.
     */
    public static final int ORDER_BY_KEY = 5;//
    /**
     * NOT IMPLEMENTED!!! - Tasks in this collection could be sortable by this field if you want to add such an option.
     */
    public static final int ORDER_BY_ROLE = 6;//
    /**
     * Tasks in this collection are sortable by this field
     */
    public static final int ORDER_BY_START_TIME = 7;
    /**
     * Tasks in this collection are sortable by this field
     */
    public static final int ORDER_BY_END_TIME = 8;
    /**
     * Tasks in this collection are sortable by this field
     */
    public static final int ORDER_BY_ELAPSED_TIME = 9;
    /**
     * NOT IMPLEMENTED!!! - Tasks in this collection could be sortable by this field if you want to add such an option.
     */
    public static final int ORDER_BY_SOURCE = 10;//
    /**
     * NOT IMPLEMENTED!!! - Tasks in this collection could be sortable by this field if you want to add such an option.
     */
    public static final int ORDER_BY_HOST = 11;//

    /**
     * set the sort order for the Tasks in this collection so they come out sorted by this field when called with the method getSortedTasks()
     *
     * @param ORDER_BY_STRING one of the ORDER_BY_* fields in this class
     * @throws Exception thrown if this string isn't parseable to an int.
     * @see setSortOrder()
     */
    public void setSortOrder(String ORDER_BY_STRING) throws Exception
    {
        int orderBy = Integer.parseInt(ORDER_BY_STRING);
        this.ORDER_BY = orderBy;
    }

    /**
     * set the sort order for the Tasks in this collection so they come out sorted by this field when called with the method getSortedTasks()
     *
     * @param ORDER_BY one of the ORDER_BY_* fields in this class
     */
    public void setSortOrder(int ORDER_BY)
    {
        this.ORDER_BY = ORDER_BY;
    }

    /**
     * returns an array of all the tasks stored in the internal hash for this collection.
     * <BR><BR>This method provides no sorting on the tasks.  Use getSortedTasks for that.
     */
    public Task[] getAllTasksArray()
    {
        return tasks.values().toArray(new Task[this.tasks.size()]);
    }

    /**
     * returns all tasks in an array from this collection sorted by whatever ordering set was set by setSortOrder().
     * <BR><BR>
     * default ordering is by the taskTag field
     * <BR><BR>
     * This is the method that viewTasks.jsp currently calls when displaying the tasks
     */
    public Task[] getSortedTasks()
    {
        Task[] t = this.getAllTasksArray();
        Arrays.sort(t);
        TaskBuilder.log(" -sorted array for returning");
        return t;
    }

    /**
     * A guaranteed list of distinct jobs, based on the stored QueryParams (limiters) and Hashtable of Tasks (tasks) in this Collection.<BR><BR>
     * If limiters has defined jobs, use those.  If not, go through all tasks in collection and make distinct list.<BR><BR>
     * Stores that array for quick retrievel by getDistinctJobTags().
     */
    private void determineDistinctJobTags()
    {
        if (this.limiters.getLimitToJob() != null)
        {
            this.distinctJobTagsArray = this.limiters.getLimitToJob();
            final String s = " -explicit jobs chosen by user used for list";
            TaskBuilder.log(s);
            return;
        }
        if (this.tasks.isEmpty())
        {
            this.distinctJobTagsArray = null;
            TaskBuilder.log(" -task hash is empty, no jobs listed");
            return;
        }

        Set<String> distinctJobTags = new HashSet<String>();
        for (Task tempTask : tasks.values())
        {
            distinctJobTags.add(tempTask.getJobTag());
        }

        String[] distinctJobTagsArray = distinctJobTags.toArray(new String[distinctJobTags.size()]);

        Arrays.sort(distinctJobTagsArray);

        this.distinctJobTagsArray = distinctJobTagsArray;

        TaskBuilder.log(" -determined distinct job list from task collection");
    }

    /**
     * returns a String array of distinct job tags for the tasks in this collection.
     */
    public String[] getDistinctJobTags()
    {
        if (this.distinctJobTagsArray == null || this.distinctJobTagsArray.length == 0)
        {
            return null;
        }
        return this.distinctJobTagsArray;
    }


    /**
     * determines based on the logic in the tms.actions table if this task and toStatus is to be updated
     * <UL>
     * <LI>recursively including the task (1),
     * <LI>ecursively NOT inculding this task (-1),
     * <LI>or not recursively at all (0)
     * </UL>
     */
    public int isRecursiveUpdate(Task task, String toStatus) throws InvalidTaskException, Exception
    {
        String[][] allAllowedActions = this.ll.getAllAllowedActions();
        String statusId = String.valueOf(task.getStatusId());
        String type = task.getType() + "";
        for (int i = 0; i < allAllowedActions.length; i++)
        {
            if (allAllowedActions[i][0].equals(statusId) &&
                    allAllowedActions[i][1].equals(toStatus) &&
                    allAllowedActions[i][2].equals(type))
            {
                return Integer.parseInt(allAllowedActions[i][5]);
            }
        }
        throw new InvalidTaskException("taskmaster.util.LookupLists:isRecursiveUpdate(): the task " + task + " cannot be changed to status " + toStatus + " according to the logic in tms.actions");
    }

    /**
     * Tests whether this Task can make the transition toStatus requested by this User.
     * <BR><BR>
     * Looks in the tms.actions table for rules, which it gets from LookupLists.
     * <BR><BR>
     * The result is a TaskToChange object, which is just a wrapper of the Task with an explanation of why the Task can make the transition, or no reason (null) if it can.
     * <BR><BR>
     * Possible reasons are:
     * <UL>
     * <LI>onlyRead - User only has read access to this Task
     * <LI>needsAdmin - User needs admin access for this transition, which he doesn't have.
     * <LI>brokenLogic - this transition is not allowed.  No row for it in tms.actions table.
     * <UL>
     *
     * @throws exception if LookupLists cannot get any actions from the db; weird error!
     */
    private TaskToChange canMakeTransition(User user, Task task, int toStatus, boolean isTop) throws Exception
    {
        String[][] allAllowedActions = this.ll.getAllAllowedActions();
        if (allAllowedActions == null)
        {
            throw new IllegalStateException("taskmaster.util.LookupLists:canMakeTransition(): allAllowedActions is null, which should never happen.  What's going on here??? Are TMS tables setup wrong? is oracle running?");
        }
        String access = user.isInRole(task.getRoleId());
        if (access == null || access.equals("read"))
        {            // no write access at all
            return new TaskToChange(task, toStatus, "onlyRead");
        }
        String statusId = String.valueOf(task.getStatusId());
        String toStatusString = String.valueOf(toStatus);
        String type = task.getType() + "";
        for (int i = 0; i < allAllowedActions.length; i++)
        {
            if (allAllowedActions[i][0].equals(statusId) &&
                    allAllowedActions[i][1].equals(toStatusString) &&
                    allAllowedActions[i][2].equals(type))
            {
                if (allAllowedActions[i][5].equals("-1") && isTop)// Darrell changed 021224 from !isTop
                {
                    return new TaskToChange(task, toStatus, "brokenLogic");    // this task isn't included in the recursion when its called (like verify tasks changed to retry by one of its preds)
                } else if (allAllowedActions[i][4].equals("0") || access.equals("admin"))
                {
                    return new TaskToChange(task, toStatus, null);    // no admin needed or has admin; TASK IS CHANGABLE!
                } else
                {
                    return new TaskToChange(task, toStatus, "needsAdmin");
                }
            }
        }
        return new TaskToChange(task, toStatus, "brokenLogic");
    }

    /**
     * A clone (identical copy) of all the tasks sent to the user in their current state at that time is stored here.
     * <BR><BR>When the user finally does confirm the Tasks he wants to change, it is compared to these tasks to make sure that they are still in the state they were in when the user Confirmed them.
     * <BR><BR>
     * **** only use this for the sync problem! its a clone! ****
     */
    private Hashtable tasksUserConfirmed = null;

    /**
     * returns an array of all tasks inside TaskToChange objects that are to be updated by the update to this task, and whether each can be updated.
     * <BR><BR>
     * public access method for the private determineUpdatability()
     * <p/>
     * <BR>if sort, uses ORDER_BY already set for the tasks it is sorting.
     * <BR>if restrictToUnique, list is kept to one copy of each task in list, instead of multiples as can happen as a result of the recursion down the tree
     *
     * @see taskmaster.job.TaskToChange
     */
    public TaskToChange[] determineUpdatability(User user, String jobTag, String taskTag, String toStatusString, boolean restrictToUniques, boolean sort, boolean depthFirst) throws InvalidTaskException, Exception
    {
        Task toChange = this.getTask(jobTag, taskTag);
        if (toChange == null)
        {
            throw new InvalidTaskException("taskmaster.TaskCollection.determineUpdatability(): Task with jobTag='" + jobTag + "' and taskTag='" + taskTag + "' does not exist in this collection");
        }
        return this.determineUpdatability(user, toChange, Integer.parseInt(toStatusString), restrictToUniques, sort, depthFirst);
    }

    /**
     * Returns all Tasks effected by the requested transition and whether or not its doable and for what reason.
     *
     * @param user              The user who requested the update
     * @param toChange          to Task the user wishes to update
     * @param toStatus          the status the user wants to update this task to
     * @param restrictToUniques whether or not the list of returned Tasks should be limited to uniques only.
     * @param sort              whether or not the list should be sorted by the ordering set by setSortOrder() or default ordering if not set.
     * @param depthFirst        when reading tree, use depth first search if true; breadth first if false.
     * @return TaskToChange object representing each Task below this one that should inherit get the same toStatus as this one.
     * @throws Exception
     */
    private TaskToChange[] determineUpdatability(User user, Task toChange, int toStatus, boolean restrictToUniques, boolean sort, boolean depthFirst) throws Exception
    {
        int i = -100;
        i = this.isRecursiveUpdate(toChange, String.valueOf(toStatus));
        i++;            // make i always positive; -1 = 0, 1 = 2 IS THIS NECESSARY ???
        Task[] descendants = null;
        switch (i)
        {
            case 2:        // recursive update including top
                descendants = this.getAllDescendants(toChange, true, depthFirst, restrictToUniques);
                break;
            case 0:        // recursive update not including top
                descendants = this.getAllDescendants(toChange, true, depthFirst, restrictToUniques);// Darrell 021224 changed false to true
                break;
            default:    // case 0+1, or error; not recursive
                descendants = new Task[1];
                descendants[0] = toChange;
        }
        if (descendants == null)
        {
            return null;
        }
        //Hashtable uniqueToChange = new Hashtable();// OLD WAY OF DOING RESTRICTTOUNIQUE.  NOW ITS DONE IN THE getAllDescendants METHOD - 020819
        TaskToChange[] taskToChange = new TaskToChange[descendants.length];
        boolean isTop = true;// if this
        for (int h = 0; h < taskToChange.length; h++)
        {//changded from descednants.length
            taskToChange[h] = this.canMakeTransition(user, descendants[h], toStatus, isTop);
            //if (restrictToUniques && uniqueToChange.get(descendants[h].getKey()) == null)// OLD WAY OF DOING RESTRICTTOUNIQUE.  NOW ITS DONE IN THE getAllDescendants METHOD - 020819
            //uniqueToChange.put(descendants[h].getKey(), new Integer(h));
            isTop = false;
        }
        /*if (restrictToUniques) {// OLD WAY OF DOING RESTRICTTOUNIQUE.  NOW ITS DONE IN THE getAllDescendants METHOD - 020819
            TaskToChange[] limitedToChange = new TaskToChange[uniqueToChange.size()];
            Integer place = null;
            for (int h=0, j=0; h<taskToChange.length; h++) {
                if ( (place = (Integer)uniqueToChange.get(taskToChange[h].task.getKey())) != null ) {
                    uniqueToChange.remove(taskToChange[h].task.getKey());
                    limitedToChange[j] = taskToChange[place.intValue()];
                    j++;
                }
            }
            taskToChange = limitedToChange;
        }*/

        this.tasksUserConfirmed = new Hashtable();
        for (int h = 0; h < taskToChange.length; h++)
        {
            this.tasksUserConfirmed.put(taskToChange[h].task.getKey(), taskToChange[h].task.clone());
        }
        if (sort)
        {
            Arrays.sort(taskToChange);
        }
        return taskToChange;
    }

    /**
     * Attempts to change all tasks whose keys are in updateThese to the status toStatusString with the comment comment.
     * <BR><BR>
     * A check is performed to see if the transition can actually be made before the performInsert or performUpdate methods are called.
     * <BR>
     * A check is also performed to see that each Task the user wants to change hasn't changed status since the last update.
     * If any of the tasks have changed statuses, the whole update is dropped and a rollback() is done to the db.
     * <BR><BR>
     * If the task has a status, an update is performed. - performUpdate()<BR>
     * If there's no status, a commit is performed. - performInsert()
     * <BR><BR>
     * After all the inserts/updates, the number of rows updated in the db is checked against the length of the updateThese array to make sure that they are equal.
     * If there is a discrepency, an exception is thrown causing a rollback().
     * <p/>
     * <BR><BR>
     * Only after all updates/inserts have been made for all the tasks in updateThese is a commit called.
     *
     * @param user           The user requesting the change
     * @param updateThese    the Task (as their key) that the user wants to change the status of.
     * @param toStatusString the status that the user wants to change updateThese to.
     * @param comment        the comment to be applied to each task changed
     * @return the number of rows updated in the db.
     * @throws TaskStatusSyncException if the status of any of the tasks being changed has changed since the last update the user saw
     * @throws Exception               if any other error occurs; a rollback() is performed.
     */
    public int changeTaskStatus(User user, String[] updateThese, int toStatusString, String comment) throws TaskStatusSyncException, Exception
    {
        try
        {
            if (updateThese == null)
            {
                return 0;
            }
            if (user == null)
            {
                throw new IllegalArgumentException("User passed as null");
            } else if (comment == null)
            {
                throw new IllegalArgumentException("Comment passed as null");
            }
            boolean hasSyncError = false;
            TaskToChange[] taskToChange = new TaskToChange[updateThese.length];

            Task[] toChange = new Task[updateThese.length];        // list of tasks that user saw
            for (int i = 0; i < updateThese.length; i++)
            {
                toChange[i] = this.getTaskUsingKey(updateThese[i]);
            }
            // find Task matching updateThese[i] in the hashtable
            boolean isTop = true;
            for (int i = 0; i < updateThese.length; i++)
            {
                // if the 2 don't match statuses, throw sync error
                taskToChange[i] = this.canMakeTransition(user, toChange[i], toStatusString, isTop);
                // match the status of the of the list the user saw with the actual status (newest from the db); alter, stop if different
//                if (toChange[i].getStatusId() != (((Task) this.tasksUserConfirmed.get(toChange[i].getKey())).getStatusId()))
//                {
//                    taskToChange[i].problem = "hasSyncError";
//                }
                if (taskToChange[i].problem != null)            // if the new status cannot be changed status for any reason
                {
                    hasSyncError = true;
                }
                isTop = false;
            }
            if (hasSyncError)
            {
                throw new TaskStatusSyncException(taskToChange);
            }
            int rowsUpdated = 0;
            isTop = true;
            for (int i = 0; i < toChange.length; i++)
            {
                if (toChange[i].getStatusId() == -100)// change this to hasStatus()
                {
                    rowsUpdated += this.performInsert(user, toChange[i], toStatusString, comment, isTop);
                } else
                {
                    rowsUpdated += this.performUpdate(user, toChange[i], toStatusString, comment, isTop);
                }
                isTop = false;
            }
            if (rowsUpdated != updateThese.length)
            {
                throw new Exception("Updated " + rowsUpdated + " rows in the db, but was only supposed to update " + updateThese.length + ".  Update cancelled, rollback issued, don't know why it did that.");
            }
            user.getConnection().commit();
            return rowsUpdated;
        } catch (TaskStatusSyncException se)
        {
            throw se;
        } catch (Exception e)
        {
            user.getConnection().rollback();
            String expl = "taskmaster.job.TaskCollection:changeTaskStatus() (a rollback has been issued): " + e;
            System.err.println(expl);
            throw new Exception(expl, e);
        }
    }

    /**
     * Attempts to change all tasks whose keys are in updateThese to the status toStatusString with the comment comment.
     * <BR><BR>
     * A check is performed to see if the transition can actually be made before the performInsert or performUpdate methods are called.
     * <BR>
     * A check is also performed to see that each Task the user wants to change hasn't changed status since the last update.
     * If any of the tasks have changed statuses, the whole update is dropped and a rollback() is done to the db.
     * <BR><BR>
     * If the task has a status, an update is performed. - performUpdate()<BR>
     * If there's no status, a commit is performed. - performInsert()
     * <BR><BR>
     * After all the inserts/updates, the number of rows updated in the db is checked against the length of the updateThese array to make sure that they are equal.
     * If there is a discrepency, an exception is thrown causing a rollback().
     * <p/>
     * <BR><BR>
     * Only after all updates/inserts have been made for all the tasks in updateThese is a commit called.
     *
     * @param user           The user requesting the change
     * @param updateThese    the Task (as their key) that the user wants to change the status of.
     * @param toStatusString the status that the user wants to change updateThese to.
     * @param comment        the comment to be applied to each task changed
     * @return the number of rows updated in the db.
     * @throws TaskStatusSyncException if the status of any of the tasks being changed has changed since the last update the user saw
     * @throws Exception               if any other error occurs; a rollback() is performed.
     */
    public boolean changeTaskStatus(User user, String taskTag, String jobTag, int toStatusString, String comment)
            throws TaskStatusSyncException, SQLException
    {
        try
        {
            if (taskTag == null)
            {
                throw new IllegalArgumentException("taskTag passed as null");
            }
            if (jobTag == null)
            {
                throw new IllegalArgumentException("jobTag passed as null");
            }
            if (user == null)
            {
                throw new IllegalArgumentException("User passed as null");
            } else if (comment == null)
            {
                throw new IllegalArgumentException("Comment passed as null");
            }
            boolean hasSyncError = false;
            TaskToChange taskToChange;

            Task toChange = this.getTask(jobTag, taskTag);
            // find Task matching updateThese[i] in the hashtable
            // if the 2 don't match statuses, throw sync error
            taskToChange = this.canMakeTransition(user, toChange, toStatusString, true);

            // match the status of the of the list the user saw with the actual status (newest from the db); alter, stop if different
//                if (toChange[i].getStatusId() != (((Task) this.tasksUserConfirmed.get(toChange[i].getKey())).getStatusId()))
//                {
//                    taskToChange[i].problem = "hasSyncError";
//                }
            if (taskToChange.problem != null)            // if the new status cannot be changed status for any reason
            {
                hasSyncError = true;
            }

            if (hasSyncError)
            {
                throw new TaskStatusSyncException(new TaskToChange[]{taskToChange});
            }
            int rowsUpdated = 0;
            if (toChange.getStatusId() == -100)// change this to hasStatus()
            {
                rowsUpdated += this.performInsert(user, toChange, toStatusString, comment, true);
            } else
            {
                rowsUpdated += this.performUpdate(user, toChange, toStatusString, comment, true);
            }
            if (rowsUpdated != 1)
            {
                throw new Exception("Updated " + rowsUpdated + " rows in the db, but expected to update " + 1 + ".  Update cancelled, rollback issued, don't know why it did that.");
            }
            user.getConnection().commit();
            return true;
        } catch (TaskStatusSyncException se)
        {
            throw se;
        } catch (Exception e)
        {
            user.getConnection().rollback();
            String expl = "taskmaster.job.TaskCollection:changeTaskStatus() (a rollback has been issued): " + e;
            System.err.println(expl);
            throw new SQLException(expl, e);
        }
    }

    /**
     * performs an update to an existing row of the db.
     * <BR><BR>
     * No rollback or commit is ever called.
     * <BR><BR>
     * A call to canMakeTransition() is made, to make sure that the change can happen.  This is redundant, but ensures that its not left out.
     *
     * @throws SQLException if there was a problem with sql or the db.
     */
    private int performUpdate(User user, Task task, int toStatus, String comment, boolean isTop) throws SQLException, Exception
    {
        String problem = null;
        if ((problem = this.canMakeTransition(user, task, toStatus, isTop).problem) != null)    // if this task cannot go to this state
        {
            throw new Exception("taskmaster.job.TaskCollection.performUpdate(): Cannot make this transition! " + task + " to " + toStatus + " because " + problem + ". How did you get this far without realizing this earlier?");
        }

        // create the prepared statement
        String sqlStmt = "UPDATE tms.task_states s1 set s1.status_id = ? , s1.end_time = sysdate , s1.source= ? , s1.host= ? , s1.note = ? where s1.job_tag = ? AND s1.task_tag = ? AND s1.run_date = ? AND s1.start_time = (select max(s2.start_time) FROM tms.task_states s2 WHERE s2.job_tag=s1.job_tag and s2.task_tag = s1.task_tag and s2.run_date = s1.run_date) AND s1.iteration = ?";
        PreparedStatement pstmtTaskUpdate = this.getPreparedStatement(user, sqlStmt);
        pstmtTaskUpdate.setInt(1, toStatus);
        pstmtTaskUpdate.setString(2, user.getUsername());
        pstmtTaskUpdate.setString(3, java.net.InetAddress.getLocalHost().getHostName());
        pstmtTaskUpdate.setString(4, comment);
        pstmtTaskUpdate.setString(5, task.getJobTag());
        pstmtTaskUpdate.setString(6, task.getTaskTag());
        pstmtTaskUpdate.setDate(7, new java.sql.Date(task.getDateOfStatus().getTime())); // day/month/year of task
        pstmtTaskUpdate.setInt(8, task.getIteration());// DS added 030305 (and to above SQL stmt; "s1.iteration = ?")

        int rowsUpdated = pstmtTaskUpdate.executeUpdate();
        pstmtTaskUpdate.close();

        return rowsUpdated;
    }

    /**
     * performs an insert of a status for a task.
     * <p/>
     * <BR><BR>
     * No rollback or commit is ever called.
     * <BR><BR>
     * A call to canMakeTransition() is made, to make sure that the change can happen.  This is redundant, but ensures that its not left out.
     *
     * @throws Exception    if the task can't make the transition, or other error.
     * @throws SQLException if there was a problem with sql or the db.
     */
    private int performInsert(User user, Task task, int toStatus, String comment, boolean isTop) throws SQLException, Exception
    {
        String problem = null;
        if ((problem = this.canMakeTransition(user, task, toStatus, isTop).problem) != null)    // if this task cannot go to this state
        {
            throw new Exception("taskmaster.job.TaskCollection.performInsert(): Cannot make this transition! " + task + " to " + toStatus + " because " + problem + ". How did you get this far without realizing this earlier?");
        }

        PreparedStatement pointer;
        if (task.getType() == 'V' || task.getType() == 'X')
        { // add an end_time if this task is of type verify or external (nothing will go back later and update it)
            String sqlStmt = "INSERT INTO tms.task_states (run_date, job_tag, task_tag, start_time, end_time, status_id, host, source, note, iteration) values ( ? , ? , ? , sysdate , sysdate, ? , ? , ? , ? , ? )";
            pointer = this.getPreparedStatement(user, sqlStmt);
        } else
        {
            String sqlStmt = "INSERT INTO tms.task_states (run_date, job_tag, task_tag, start_time, status_id, host, source, note, iteration) values ( ? , ? , ? , sysdate , ? , ? , ? , ? , ? )";
            pointer = this.getPreparedStatement(user, sqlStmt);
        }


        pointer.setDate(1, new java.sql.Date(task.getDateOfStatus().getTime()));    // day/month/year of status
        pointer.setString(2, task.getJobTag());
        pointer.setString(3, task.getTaskTag());
        pointer.setInt(4, toStatus);
        pointer.setString(5, java.net.InetAddress.getLocalHost().getHostName());
        pointer.setString(6, user.getUsername());
        pointer.setString(7, comment);
        pointer.setInt(8, task.getIteration());// DS added 030305 (and to the 2 sql above I added "iteration"..."?")
        int rowsUpdated = pointer.executeUpdate();
        pointer.close();
        return rowsUpdated;
    }

    /**
     * takes an array of Tasks and returns another array that preserves the order of the original but doesn't have repeat Tasks in it.
     * <BR>The first instance of a Task in the array is kept while the rest are left out.
     * <p/>
     * <BR><BR>
     * - the restricting to unique algorithm preserves the tree like return value of the descendant/pred list<BR>
     * &nbsp;&nbsp;&nbsp;- since the list is returned with repeats<BR>
     * &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;- hash each descendant by key, storing the place in the array where this Task was found<BR>
     * &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;- only hash when its not already there; referencing first instance of the Task in the tree that's found<BR>
     * &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;- create final array to be of size of the hashtable (it is to have no duplicates)<BR>
     * &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;- loop through each task in the full descendant array (the one with repeats)<BR>
     * &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;- if that task exists in the hash of unique tasks<BR>
     * &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;- get its place in the descendant/pred array<BR>
     * &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;- put that place (toRestrict[place.intValue()]) into toReturn array (restricted[j])<BR>
     * &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;- remove that entry from the hash since its now been entered in the unique array<BR>
     */
    private Task[] restrictToUnique(Task[] toRestrict) throws Exception
    {
        Hashtable uniques = new Hashtable();
        for (int h = 0; h < toRestrict.length; h++)
        {
            if (uniques.get(toRestrict[h].getKey()) == null)
            {
                uniques.put(toRestrict[h].getKey(), new Integer(h));
            }
        }

        Task[] restricted = new Task[uniques.size()];
        Integer place = null;
        for (int h = 0, j = 0; h < toRestrict.length; h++)
        {
            if ((place = (Integer) uniques.get(toRestrict[h].getKey())) != null)
            {
                uniques.remove(toRestrict[h].getKey());
                restricted[j] = toRestrict[place.intValue()];
                j++;
            }
        }
        return restricted;
    }

    /**
     * returns array of all tasks that keep the given task from being ready to be run.
     * <BR><BR>This method provides access to the private getWaitingFor() method.
     *
     * @param includeCompleted if true list includes the Tasks that are in the completedCategory, if false they're not included in the list.
     */
    public Task[] getWaitingFor(Task task, boolean includeCompleted, boolean restrictToUniques) throws Exception
    {
        return this.getWaitingFor(task, true, false, includeCompleted, restrictToUniques);
    }

    /**
     * returns array of all tasks that keep the given task from being ready.
     *
     * @param reverseOrder     if true, the order of Tasks is reversed; if false the order is kept in the order they're found
     * @param includeCompleted if true list includes the Tasks that are in the completedCategory, if false they're not included in the list.
     */
    private Task[] getWaitingFor(Task task, boolean predsAndDeps, boolean reverseOrder, boolean includeCompleted, boolean restrictToUniques) throws Exception
    {
        if (task == null)
        {
            throw new Exception("taskmaster.job.TaskCollection.getWaitingFor(): passed Task is null...can't do that!");
        }
        Vector blockers = new Vector();
        if (predsAndDeps)
        {
            this.addBlockedDeps(task, blockers, includeCompleted);
        }
        TaskEnumeration te = new TaskEnumeration(blockers.elements());
        Task[] blockerTask = new Task[blockers.size()];
        if (reverseOrder)
        {
            for (int i = blockerTask.length - 1; i >= 0 && te.hasMoreTasks(); i--)
            {
                blockerTask[i] = te.nextTask();
            }
        } else
        {
            for (int i = 0; i < blockerTask.length && te.hasMoreTasks(); i++)
            {
                blockerTask[i] = te.nextTask();
            }
        }
        if (restrictToUniques)
        {
            return this.restrictToUnique(blockerTask);
        } else
        {
            return blockerTask;
        }
    }

    /**
     * Starting at this task in the tree, add elements to 'blockers' of all tasks up the tree.
     * <BR><BR>This method recursively calls itself for each Task.
     * If includeCompleted is true, all tasks up the tree are put into blockers; if false only tasks that aren't completed are added.
     */
    private void addBlockedDeps(Task task, Vector blockers, boolean includeCompleted) throws Exception
    {
        TaskEnumeration tEnum = task.getAllDependencies();
        tEnum.addAnother(task.getPredecessorTask());
        Task walker = null;
        while (tEnum.hasMoreTasks())
        {
            walker = tEnum.nextTask();
            if (includeCompleted || !walker.isCompleted())
            {
                blockers.add(walker);
            }
            this.addBlockedDeps(walker, blockers, includeCompleted);
        }
    }


    /**
     * returns array of Tasks that are all descendants down the tree for this task
     *
     * @param depthFirst if true, use depthFirst search down tree; if false use breadth first
     */
    public Task[] getAllDescendants(Task task, boolean includeSelf, boolean depthFirst, boolean restrictToUniques) throws Exception
    {//, boolean depthFirst
        Vector allDescendants = new Vector();
        if (depthFirst)
        {
            this.getAllDescendantsDepth(task, allDescendants, includeSelf);
        } else
        {
            this.getAllDescendantsBreadth(task, allDescendants, includeSelf);
        }
        if (allDescendants.size() == 0)// change to return null
        {
            return null;
        }
        Task[] descendantArray = new Task[allDescendants.size()];
        for (int i = 0; i < descendantArray.length; i++)
        {
            descendantArray[i] = (Task) allDescendants.elementAt(i);
        }
        if (restrictToUniques)
        {
            return this.restrictToUnique(descendantArray);
        }
        return descendantArray;
    }

    /**
     * Perform a depth first search down the tree from toChange, adding all descendants to allDescendants
     *
     * @param includeTop include toChange in the vector allDescendants
     */
    private void getAllDescendantsDepth(Task toChange, Vector allDescendants, boolean includeTop) throws Exception
    {
        //System.err.println("beginning recursion (collection) for "+ toChange +" includTop: "+ includeTop);
        TaskEnumeration tEnum = toChange.getAllChildren();
        Task walker = null;
        if (includeTop)
        {
            allDescendants.add(toChange);
        }
        while (tEnum.hasMoreTasks())
        {
            walker = tEnum.nextTask();
            this.getAllDescendantsDepth(walker, allDescendants, true);
        }
    }

    /**
     * Perform a breadth first search down the tree from toChange, adding all descendants to allDescendants
     *
     * @param includeTop include toChange in the vector allDescendants
     */
    private void getAllDescendantsBreadth(Task toChange, Vector allDescendants, boolean includeTop) throws Exception
    {
        Vector queue = new Vector();
        if (includeTop)
        {
            allDescendants.add(toChange);
        }

        TaskEnumeration children = toChange.getAllChildren();
        while (children.hasMoreTasks())
        {
            queue.add(children.nextTask());
        }

        Task node = null;
        while (queue.size() != 0)
        {
            node = (Task) queue.elementAt(0);
            allDescendants.add(node);
            queue.remove(0);
            children = node.getAllChildren();
            while (children.hasMoreTasks())
            {
                queue.add(children.nextTask());
            }
        }
    }

    public boolean canMakeTransition(User user, Task task, String key)
    {
        final TaskToChange toChange;
        try
        {
            toChange = canMakeTransition(user, task, Integer.parseInt(key), false);
        } catch (Exception e)
        {
            e.printStackTrace();
            return false;
        }
        return toChange.canBeChanged();
    }


    void limitAfterUpdate(Set<String> limitToCategories)
    {
        for (Task refTask : this.tasks.values())
        {
            if (limitToCategories != null && !limitToCategories.isEmpty() &&
                    !limitToCategories.contains(refTask.getQuickCategory()))
            {
                this.tasks.remove(refTask.getKey());
            }
        }
    }
}